Skip to content

Propose a path extension type - #115

Open
apg wants to merge 2 commits into
cedar-policy:mainfrom
apg:apg/path-extension-type
Open

Propose a path extension type#115
apg wants to merge 2 commits into
cedar-policy:mainfrom
apg:apg/path-extension-type

Conversation

@apg

@apg apg commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Rendered

Checklist

  • Added RFC to text/SUMMARY.md in numerical order

- Derived from discussion on @cedar-policy/rfcs cedar-policy#103
- path(..., ..., ...) becomes container to call .matches and
  .prefixMatches against

Signed-off-by: Andrew Gwozdziewycz <andrew.gwozdziewycz@docker.com>
@apg
apg force-pushed the apg/path-extension-type branch from f4f7c6b to 2f41532 Compare June 5, 2026 23:31

@patjakdev patjakdev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder about the feasibility of generalizing the proposed path type to an ordered list type, where path becomes just a list[string].

The biggest wart that I think can think of is that list[string] would be the only type to allow for the special wildcard syntax when doing matching. A new any keyword could potentially be added to allow for wildcards in lists of other types.


## Summary

This RFC proposes a `path` extension type for Cedar. A path represents a variable-length, ordered sequence of strings — for example, the components of a filesystem path, S3 object key, URL path, MQTT topic, or reversed hostname. The `path` type exposes two matching methods, `.matches()` and `.prefixMatches()`, whose arguments must be string literals. This restriction keeps Cedar's SMT-based policy analysis decidable and efficient. String splitting and delimiter handling are intentionally out of scope; path values are constructed from pre-parsed components supplied by the application.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might propose prefixLike instead of prefixMatches for symmetry with the like operator.

You might also consider a prefixEquals for situations where * should be treated as a literal asterisk rather than a wildcard without the need to escape it.

@apg apg Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the Like vs Matches. I think Matches might work better given its relationship to structual pattern matching, though, you're right about the name symmetry--prefixLike might be more ergonomic. Open to other feedback here!


#### `.matches(pattern1, ..., patternN)`

Returns `true` if the path has **exactly** `N` components and each matches the corresponding pattern:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably, this method errors if a non-string is passed as an argument.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. That would make sense! I'll have to update this.

path(resource.bucket, context.tenant) // dynamic components
```

Arguments may be any string-typed expression — they are not required to be literals. Strict validation requires each argument to have type `string`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. This might be the first extension type constructor that doesn't require a string literal for strict validation since it's not possible to pass an invalid value.

Comment thread text/0115-path-extension-type.md

### Schema

```json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include the Cedar format as well?

Comment thread text/0115-path-extension-type.md Outdated

Records with positional fields work for paths whose length is fixed by the schema. They fail for variable-length paths — S3 keys, URL paths, MQTT topics — which is exactly what this RFC targets.

### Alternative C: String attributes with `like`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the same thing as Alternative F? I suppose alternatives A and B are also the same thing as Alternative F.


- **`.get()` inclusion.** Should element access be included here or deferred to a follow-on? The primary value of `path` comes from `.matches()` and `.prefixMatches()`; `.get()` adds convenience but also implementation and specification surface area.

- **Wildcard semantics.** Pattern matching follows Cedar's `like` semantics: `*` matches zero or more characters within a single component and does not cross component boundaries. This means `path("foo", "bar").matches("*")` is `false` (length 2, not 1). The interaction between zero-length matches and the hostname use case (where `*.github.com` should require a non-empty subdomain) should be confirmed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand what this is getting at.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, matches("*") is strict on the number of components. In the example above path("foo", "bar").matches("*", "bar") is fine because it has two components. The variant prefixMatches supports the variable number of components.

Comment thread text/0115-path-extension-type.md Outdated

- **Wildcard semantics.** Pattern matching follows Cedar's `like` semantics: `*` matches zero or more characters within a single component and does not cross component boundaries. This means `path("foo", "bar").matches("*")` is `false` (length 2, not 1). The interaction between zero-length matches and the hostname use case (where `*.github.com` should require a non-empty subdomain) should be confirmed.

- **Empty path behavior.** Should `path()` — a zero-component path — be a valid value? `path().matches()` and `path().prefixMatches()` would both return `true`, which seems correct, but edge cases in entity data deserve consideration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine to me.

If we were to generalize path into list, though, it would be a problem because the type would be unknown.

@apg

apg commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

I wonder about the feasibility of generalizing the proposed path type to an ordered list type, where path becomes just a list[string].

The biggest wart that I think can think of is that list[string] would be the only type to allow for the special wildcard syntax when doing matching. A new any keyword could potentially be added to allow for wildcards in lists of other types.

I think this is something @emina would have to look at. I simply based this off of this comment #103 (comment). It seems as though general lists would be analyzable if they worked like the proposal (only integer literals for accessing elements, no length).

Signed-off-by: Andrew Gwozdziewycz <andrew.gwozdziewycz@docker.com>
@apg
apg force-pushed the apg/path-extension-type branch from c250ee9 to 1ba6996 Compare June 23, 2026 00:32
@chaluli

chaluli commented Jul 25, 2026

Copy link
Copy Markdown

Rather than adding a new path type. I think we can enriching the regular language matching mechanism we currently using in string matching (the like operator) to support the glob matching this PR is trying to support.

I concretely propose the following syntax:

string.matches(glob("glob literal", "delimiter"))      # globs support both * and ** patterns (1 or 0+ components)
string.matches(re("regular language literal"))         # optional but is the most general matching mechanism
string.componentAt(n_index, "delimiter") == (expr)     # resource.domain.componentAt(1, '.') == principal.team
string.countComponents("delimiter") OP (Integer)       # resource.domain.countCompoents('.') <= 4

These all remain analyzable because like the like operator, they can be directly compiled into string operations that are supported in SMTLib (mostly str.in_re(s, R) where R is a regular language literal, which is exactly what the like operator already compiles to when analyzed). Note, we mean that the regular language literal is equivalent to an NFA not a regular expression (which may be non-regular due to forward/backward lookups, groups, etc.)

my_string.matches(re("R")) becomes str.in_re(my_string, R) (here we assume that R is a regular language as described by SMTLib standard).

my_string.matches(glob("*.github.com", ".")) becomes str.in_re(my_string, "[^\.]*\.github\.com") Behaves just like the like operator but makes use of delimiter.

The string.componentAt(n_index, "delimiter") and string.countComponents("delimiter") unfortunately requires the delimiter be a single character (or at least not e.g., .. or // where there are repeating segments). The first makes use of str.substr/indexof (formula grows linearly in the value of n_index, fine for n_index = 0, 1, 2, 3, 4, etc. but very bad for n_index = 10000).

@emina

emina commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

The string.componentAt(n_index, "delimiter") and string.countComponents("delimiter") unfortunately requires the delimiter be a single character (or at least not e.g., .. or // where there are repeating segments). The first makes use of str.substr/indexof (formula grows linearly in the value of n_index, fine for n_index = 0, 1, 2, 3, 4, etc. but very bad for n_index = 10000).

I don’t think we should use st.substr/indexof or anything that brings the theory of integers into the translation. We’ve carefully avoided this so far, because Cedar itself doesn’t have integers: it uses 64-bit vectors. So, unless n_index is a literal, the translation has to go back and forth between the two representations, which leads to poor performance in practice. Or if it is, then the formula grows linearly with n_index (as the path representation’s formula would).

Taking a step back, maybe we should re-consider an MVP solution that handles most use cases: like matching with a delimiter. This would introduce a variation of the like operator, along the lines of <expr> like(<delim>) <pattern>, or whatever syntax we find most palatable. This translates directly into a str.in_re check, where the <delim> character is treated as a literal in the pattern. While not as expressive as either paths or globing, it avoids the theory of integers and/or linear scaling of the formula with index size. It should perform comparably to like without delimiters. And it should be easier to implement / formalize / maintain than the alternatives.

@chaluli

chaluli commented Jul 26, 2026

Copy link
Copy Markdown

Taking a step back, maybe we should re-consider an MVP solution that handles most use cases: like matching with a delimiter. This would introduce a variation of the like operator, along the lines of <expr> like(<delim>) <pattern>, or whatever syntax we find most palatable. This translates directly into a str.in_re check, where the <delim> character is treated as a literal in the pattern. While not as expressive as either paths or globing, it avoids the theory of integers and/or linear scaling of the formula with index size. It should perform comparably to like without delimiters. And it should be easier to implement / formalize / maintain than the alternatives.

I agree with this. The like matching with a delimiter I think is the biggest improvement to effort ratio without introducing any new operators into the SMT encoding for analyzability. The last two could be done but put us into a situation where we need to increase the kinds of SMT operators in our encoding and/or increase the size of the produced formula proportional to the value of a literal (not good).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants